﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;
using System.Management.Automation.Runspaces;

namespace LinqToPowershell
{
    [Cmdlet("Invoke", "CSharp")]
    public class InvokeCSharpCmdlet : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
        public string Statement { get; set; }

        [Parameter(Position = 1)]
        public string Language { get; set; }

        [Parameter()]
        public SwitchParameter PassThru { get; set; }

        [Parameter()]
        public SwitchParameter Keep { get; set; }

        private CodeDomProvider m_compiler;
        private CompilerParameters m_compileOptions;
        private Assembly[] m_assemblies;
        private IEnumerable<PSCapture> m_psVars = new List<PSCapture>();
        private string m_tempNS = "LinqToPowershell.Temp";
        private string m_tempClass = "TempClass" + DateTime.Now.Ticks;
        private string m_tempMethod = "RunMe";

        protected override void EndProcessing()
        {
            SetupCompiler();

            var code = GenerateCode();
            var assembly = Compile(code);
            if (assembly == null)
                return;

            var tempClass = assembly.GetType(m_tempNS + "." + m_tempClass);
            CapturePSVars(tempClass);
            Run(tempClass);
            PushVarsToPS(tempClass);
        }
                
        private void SetupCompiler()
        {
            if (Language != null && false)  // C# only for now
            {
                m_compiler = CodeDomProvider.CreateProvider(Language);
            }
            else
            {
                var dict = new Dictionary<string, string>();
                dict.Add("CompilerVersion", "v3.5");
                m_compiler = new CSharpCodeProvider(dict);
            }

            m_assemblies = AppDomain.CurrentDomain.GetAssemblies();
            m_compileOptions = new CompilerParameters(m_assemblies.Select(a => a.Location).ToArray())
            {
                GenerateInMemory = true,
                TreatWarningsAsErrors = false,
                TempFiles = new TempFileCollection() { KeepFiles = Keep.IsPresent },
                WarningLevel = 0
            };            
        }

        private void Run(Type tempClass)
        {
            var runMe = tempClass.GetMethod(m_tempMethod);
            runMe.Invoke(null, new object[] { });
        }


        private void CapturePSVars(Type tempClass)
        {
            foreach (var psVar in m_psVars)
            {
                var field = tempClass.GetField(psVar.Name, BindingFlags.Public | BindingFlags.Static);
                field.SetValue(null, psVar.Value);
            }
        }

        private void PushVarsToPS(Type tempClass)
        {
            foreach (var psVar in m_psVars)
            {
                var field = tempClass.GetField(psVar.Name, BindingFlags.Public | BindingFlags.Static);
                var newValue = field.GetValue(null);
                base.SessionState.PSVariable.Set(psVar.Name, newValue);
            }
        }

        private List<CodeNamespaceImport> GenerateUsings()
        {
            var usings = new List<CodeNamespaceImport>();

            var psUsings = base.GetVariableValue("using") as string[];
            IEnumerable<string> namespaces;
            if (psUsings != null)
            {
                namespaces = psUsings;
            }
            else
            {                
                namespaces = (from a in m_assemblies
                              where !String.IsNullOrEmpty(a.Location)
                              select (from t in a.GetTypes()                                    
                                     select t.Namespace)
                             ).Aggregate(new HashSet<string>(), (a, b) => { a.UnionWith(b); return a; });
            }

            namespaces = (from string ns in namespaces
                          where !String.IsNullOrEmpty(ns)
                              && Char.IsLetter(ns[0])
                          select ns)
                          .Distinct();
            foreach (var ns in namespaces)
            {
                usings.Add(new CodeNamespaceImport(ns));
            }

            return usings;
        }

        private void AddUsings(List<CodeNamespaceImport> usings, IEnumerable<string> namespaces)
        {            
            namespaces = namespaces
                .Distinct()
                .Where(ns => !String.IsNullOrEmpty(ns) && Char.IsLetter(ns[0]));

            
        }
        
        private CodeCompileUnit GenerateCode()
        {
            var ns = CreateNamespace();           
            
            var ccu = new CodeCompileUnit();
            ccu.Namespaces.Add(ns);            
            return ccu;
        }

        private CodeNamespace CreateNamespace()
        {
            var tempNS = new CodeNamespace(m_tempNS);

            var usings = GenerateUsings();
            tempNS.Imports.AddRange(usings.ToArray());
            
            var tempClass = new CodeTypeDeclaration(m_tempClass)
            {
                IsClass = true,
                TypeAttributes = TypeAttributes.Public,
                Attributes = MemberAttributes.Public | MemberAttributes.Static
            };
            tempNS.Types.Add(tempClass);
            AddPSVars(tempClass);

            var runMe = new CodeMemberMethod()
            {
                Attributes = MemberAttributes.Public | MemberAttributes.Static,
                Name = m_tempMethod,
                ReturnType = new CodeTypeReference(typeof(void))
            };
            tempClass.Members.Add(runMe);
            
            runMe.Statements.Add(new CodeSnippetStatement(Statement));
            
            return tempNS;
        }

        private List<string> ParsePSVars()
        {
            var list = new List<string>();
            var copy = Statement.ToLower();
            int offset = 0;

            // quick & VERY dirty parse for $variable uses
            Func<char, bool> IsSeparator = c => !Char.IsLetterOrDigit(c);
            Func<char, bool> IsVariableStart = c => Char.IsLetter(c);
            Func<char, bool> IsVariable = c => Char.IsLetter(c);

            for (int i = 0; i < copy.Length; ++i)
            {
                if (copy[i] == '$'
                    && (i == 0 || IsSeparator(copy[i - 1]))
                    && (i + 1 < Statement.Length && IsVariableStart(copy[i + 1]))
                    )
                {
                    Statement = Statement.Remove(i + offset--, 1);
                    var sb = new StringBuilder();
                    for (++i; IsVariable(copy[i]); ++i)
                    {
                        sb.Append(copy[i]);
                    }
                    list.Add(sb.ToString());
                }
            }

            return list;
        }

        private void AddPSVars(CodeTypeDeclaration tempClass)
        {
            Func<PSObject, string> GetName = o => o.Properties["Name"].Value as string;
            Func<PSObject, object> GetValue = o => o.Properties["Value"].Value;

            var varNames = ParsePSVars().Aggregate((a, b) => a + "," + b);
            m_psVars = from v in base.InvokeCommand.InvokeScript("get-variable " + varNames, false, PipelineResultTypes.None, null)
                       where v.TypeNames.Contains("System.Management.Automation.PSVariable")
                          && m_compiler.IsValidIdentifier(GetName(v))
                       select new PSCapture
                       {
                           Name = GetName(v),
                           Value = GetValue(v),
                           TypeName = GetValue(v) == null ? "" : GetValue(v).GetType().FullName
                       };

            foreach (var variable in m_psVars)
            {
                tempClass.Members.Add(new CodeMemberField(variable.TypeName, variable.Name) {
                    Attributes = MemberAttributes.Public | MemberAttributes.Static
                });
            }
        }

        private Assembly Compile(CodeCompileUnit ccu)
        {            
            var results = m_compiler.CompileAssemblyFromDom(m_compileOptions, ccu);

            if (results.Errors.HasErrors)
            {
                foreach (var line in results.Output)
                {
                    WriteWarning(line);                    
                }
                return null;
            }            
            return results.CompiledAssembly;
        }
    }

    public class PSCapture
    {
        public string Name;
        public object Value;
        public string TypeName;
    }
}
